昨天我們深入學習了所有權系統,你可能已經發現一個問題:如果每次使用資料都要轉移所有權,那程式設計會變得非常麻煩。今天,我們要學習 Rust 的另一個核心概念 - 借用(Borrowing)與參考(References),這是讓所有權系統變得實用的關鍵。
還記得昨天的任務管理系統嗎?我們必須不斷地返回所有權才能繼續使用資料:
// 昨天的麻煩寫法
fn main() {
let task = create_task();
let task = process_task(task); // 必須取回所有權
let task = update_task(task); // 又要取回一次
let task = complete_task(task); // 再取回一次...
println!("最終狀態: {:?}", task);
}
這樣寫太累人了!讓我們用借用來解決這個問題。
參考(Reference)允許你使用值但不擁有它:
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // &s1 創建一個參考
println!("'{}' 的長度是 {}", s1, len); // s1 仍然有效!
}
fn calculate_length(s: &String) -> usize {
s.len()
} // s 離開作用域,但因為它不擁有值,所以什麼都不會發生
堆疊(Stack) 堆積(Heap)
┌─────────┐ ┌─────────────┐
│ s1 │ ──────────> │ "hello" │
├─────────┤ └─────────────┘
│ &s1 │ ──────────────────┘
└─────────┘ (參考指向同一個值)
Rust 對借用有嚴格的規則,這些規則在編譯時就會檢查:
fn main() {
let s = String::from("hello");
let r1 = &s; // 沒問題
let r2 = &s; // 沒問題
let r3 = &s; // 沒問題
println!("{}, {}, {}", r1, r2, r3);
}
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
// let r2 = &mut s; // 錯誤!不能有兩個可變參考
r1.push_str(", world");
println!("{}", r1);
}
fn main() {
let mut s = String::from("hello");
let r1 = &s; // 不可變參考
let r2 = &s; // 另一個不可變參考
// let r3 = &mut s; // 錯誤!不能同時有可變參考
println!("{} and {}", r1, r2);
// r1 和 r2 的作用域在這裡結束
let r3 = &mut s; // 現在可以創建可變參考了
r3.push_str(", world");
println!("{}", r3);
}
Rust 2018 引入了 NLL(Non-Lexical Lifetimes),讓參考的作用域更智能:
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
// r1 和 r2 在這裡之後不再使用,作用域結束
let r3 = &mut s; // 可以創建可變參考了
r3.push_str(", world");
println!("{}", r3);
}
#[derive(Debug)]
struct Task {
title: String,
description: String,
completed: bool,
}
impl Task {
fn new(title: String, description: String) -> Self {
Task {
title,
description,
completed: false,
}
}
// 使用 &self - 不可變借用
fn display(&self) {
println!("任務: {}", self.title);
println!("描述: {}", self.description);
println!("狀態: {}", if self.completed { "已完成" } else { "進行中" });
}
// 取得資訊但不修改
fn get_title(&self) -> &str {
&self.title
}
fn is_completed(&self) -> bool {
self.completed
}
}
fn main() {
let task = Task::new(
String::from("學習借用"),
String::from("理解 Rust 的借用機制")
);
task.display(); // 借用 task
let title = task.get_title(); // 再次借用
println!("任務標題: {}", title);
// task 仍然可用!
println!("任務狀態: {:?}", task);
}
impl Task {
// 使用 &mut self - 可變借用
fn complete(&mut self) {
self.completed = true;
println!("任務 '{}' 已完成!", self.title);
}
fn update_description(&mut self, new_desc: String) {
self.description = new_desc;
}
fn add_note(&mut self, note: &str) {
self.description.push_str("\n備註: ");
self.description.push_str(note);
}
}
fn main() {
let mut task = Task::new(
String::from("學習借用"),
String::from("理解 Rust 的借用機制")
);
task.add_note("這很重要!"); // 可變借用
task.complete(); // 另一個可變借用
task.display(); // 不可變借用
}
切片讓你參考集合的一部分:
fn main() {
let s = String::from("hello world");
let hello = &s[0..5]; // 或 &s[..5]
let world = &s[6..11]; // 或 &s[6..]
let whole = &s[..]; // 整個字串的切片
println!("{} {}", hello, world);
}
// 更實用的例子
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3]; // [2, 3]
assert_eq!(slice, &[2, 3]);
// 使用切片的函式
let sum = sum_slice(slice);
println!("總和: {}", sum);
}
fn sum_slice(slice: &[i32]) -> i32 {
let mut sum = 0;
for &item in slice {
sum += item;
}
sum
}
struct TaskManager {
tasks: Vec<Task>,
}
impl TaskManager {
fn new() -> Self {
TaskManager { tasks: Vec::new() }
}
fn add_task(&mut self, task: Task) {
self.tasks.push(task);
}
// 返回不可變參考
fn get_task(&self, index: usize) -> Option<&Task> {
self.tasks.get(index)
}
// 返回可變參考
fn get_task_mut(&mut self, index: usize) -> Option<&mut Task> {
self.tasks.get_mut(index)
}
// 同時借用多個
fn get_two_tasks_mut(&mut self, i: usize, j: usize) -> Option<(&mut Task, &mut Task)> {
if i == j {
return None; // 不能借用同一個任務兩次
}
// 分割可變借用
if i < j {
let (left, right) = self.tasks.split_at_mut(j);
Some((&mut left[i], &mut right[0]))
} else {
let (left, right) = self.tasks.split_at_mut(i);
Some((&mut right[0], &mut left[j]))
}
}
}
// 這不會編譯!
fn dangle() -> &String {
let s = String::from("hello");
&s // s 離開作用域會被丟棄,參考會懸垂
}
// 正確的做法:返回擁有的值
fn no_dangle() -> String {
let s = String::from("hello");
s // 移動所有權
}
fn main() {
let r;
{
let x = 5;
r = &x; // 錯誤!x 的生命週期不夠長
}
// println!("r: {}", r); // x 已經被丟棄
}
// 正確的做法
fn main() {
let x = 5;
let r = &x;
println!("r: {}", r); // x 仍然有效
}
// 優先使用 &str 而不是 &String
fn process_text(text: &str) {
println!("處理: {}", text);
}
// 優先使用 &[T] 而不是 &Vec<T>
fn process_numbers(numbers: &[i32]) {
for &n in numbers {
println!("{}", n);
}
}
fn main() {
let s = String::from("hello");
process_text(&s); // String 可以自動轉換為 &str
let v = vec![1, 2, 3];
process_numbers(&v); // Vec<T> 可以自動轉換為 &[T]
}
impl Task {
// 使用 &self - 只讀取
fn get_info(&self) -> String {
format!("{}: {}", self.title, self.description)
}
// 使用 &mut self - 需要修改
fn update(&mut self, title: String) {
self.title = title;
}
// 使用 self - 需要消耗
fn into_completed(mut self) -> Self {
self.completed = true;
self
}
}
fn find_longest_word(text: &str) -> &str {
// 實作這個函式
// 提示:使用 split_whitespace() 和迭代器
}
fn swap_tasks(tasks: &mut Vec<Task>, i: usize, j: usize) {
// 交換兩個任務的位置
// 注意處理邊界情況
}
struct BorrowChecker<'a> {
data: &'a str,
}
impl<'a> BorrowChecker<'a> {
fn new(data: &'a str) -> Self {
// 實作
}
fn get_data(&self) -> &str {
// 實作
}
}
明天我們將學習生命週期(Lifetimes),這是 Rust 確保參考總是有效的機制。
今天的內容可能需要多練習幾次才能完全掌握,這很正常!借用規則一開始看起來很嚴格,但它們是 Rust 能夠保證記憶體安全的 secret sauce !